Skip to content

Comments

Fix/kakao pay#153

Merged
ddhi7 merged 2 commits intodevelopfrom
fix/kakaoPay
Feb 17, 2026
Merged

Fix/kakao pay#153
ddhi7 merged 2 commits intodevelopfrom
fix/kakaoPay

Conversation

@ddhi7
Copy link
Contributor

@ddhi7 ddhi7 commented Feb 17, 2026

  1. 결제 실패, 취소시 프론트 도메인으로 리다이렉트
  2. 결제 미완료된 티켓 15분 경과시 스케줄러로 stopPayment호출하여 재고 복구
    (기존의 프론트 측에서 뒤로가기를 누르면 cancel api 호출하는 방식도 구현해봤으나 너무 복잡한 관계로 백엔드 단에서 스케줄러 처리하기로 했습니다! 문제점: 사용자가 강제로 뒤로가기를 눌렀을때 재고 복구가 바로 반영이 안됨. 15분 이상 지나야 재고 복구)

Summary by CodeRabbit

  • New Features

    • Added automatic cleanup of expired temporary ticket reservations after 15 minutes to prevent orphaned bookings.
  • Bug Fixes

    • Enhanced payment cancellation and failure handling with improved redirect behavior.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This PR refactors KakaoPayController to replace injected URL configuration with a hardcoded FRONT_DOMAIN constant and converts cancel/fail endpoints to use HTTP redirects instead of ApiResponse returns. It introduces a new TempTicketExpireScheduler that automatically cancels payments for tickets pending longer than 15 minutes. KakaoPayBusinessService.stopPayment now returns playId, and TempTicketRepository gains a specialized query method for expired tickets.

Changes

Cohort / File(s) Summary
Kakao Pay Controller Refactoring
src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java, src/main/resources/application.yml
Replaced injected URL with hardcoded FRONT_DOMAIN constant; converted cancel() and fail() methods from ApiResponse returns to void with HttpServletResponse redirects; removed front.kakaopay.complete-url configuration.
Service Signature Update
src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java
Changed stopPayment() return type from void to Long to return playId; maintains existing control flow but yields playId on all code paths.
Ticket Expiration Scheduler
src/main/java/cc/backend/scheduler/TempTicketExpireScheduler.java
New scheduled component running every 60 seconds to find and cancel payments for tickets pending longer than 15 minutes; integrates with KakaoPayBusinessService and logs per-ticket results.
Repository Query Update
src/main/java/cc/backend/ticket/repository/TempTicketRepository.java
Replaced findByReservationStatusAndCreatedAtBefore() with findExpiredPendingTickets() using JPQL @Query annotation for cleaner expired ticket queries.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as TempTicketExpireScheduler
    participant Repo as TempTicketRepository
    participant DB as Database
    participant Service as KakaoPayBusinessService

    Scheduler->>Scheduler: Calculate expiration threshold<br/>(Seoul time - 15 min)
    activate Scheduler
    
    Scheduler->>Repo: findExpiredPendingTickets(expireTime)
    activate Repo
    Repo->>DB: Query PENDING tickets<br/>created before threshold
    activate DB
    DB-->>Repo: Return expired tickets
    deactivate DB
    Repo-->>Scheduler: List<TempTicket>
    deactivate Repo
    
    alt Tickets found
        Scheduler->>Scheduler: Iterate over each ticket
        Scheduler->>Service: stopPayment(partnerOrderId)
        activate Service
        Service->>DB: Restore inventory &<br/>update status
        activate DB
        DB-->>Service: Success
        deactivate DB
        Service-->>Scheduler: Return playId
        deactivate Service
        Scheduler->>Scheduler: Log successful expiration
    else No tickets found
        Scheduler->>Scheduler: Exit early
    end
    
    deactivate Scheduler
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #112: Modifies KakaoPayController's cancel/fail endpoints and KakaoPayBusinessService.stopPayment for payment cancellation flow—directly impacts same methods in this PR.
  • #140: Updates KakaoPayBusinessService.stopPayment error handling and status updates—touches the same service method signature changes.

Suggested reviewers

  • yhi9839

Poem

🐰 Tickets expire like clover in the sun,
A scheduler hops by every sixty-one second, one by one,
Pending payments fall like autumn leaves,
Swept away clean—the system breathes,
No more forgotten orders left behind! 🎫✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fix/kakao pay' is vague and generic, using a non-descriptive prefix that doesn't convey the specific changes made in the changeset. Use a more descriptive title that clearly summarizes the main changes, such as 'Redirect payment failures to frontend and add scheduler for expired tickets' or 'Implement payment callback redirects and auto-expiry scheduler'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the main work items but lacks complete alignment with the template; it is missing explicit issue references (#⃣) and doesn't follow the structured sections as defined in the repository template.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/kakaoPay

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java (1)

36-47: ⚠️ Potential issue | 🟠 Major

Missing error handling in approve — a failure results in a raw 500 error instead of a user-friendly redirect.

Unlike cancel and fail, the approve endpoint has no try-catch. If completePayment throws, the user sees a server error page. Wrap it consistently to redirect with an error indicator.

🐛 Proposed fix
     public void approve(`@Parameter`(description = "ticketId 입니다") `@RequestParam`("partner_order_id") String partnerOrderId,
                                                     `@RequestParam`("pg_token") String pgToken,
                                                            HttpServletResponse response) throws IOException {
+        try {
             KakaoPayResultResponseDTO result =
                     kakaoPayBusinessService.completePayment(partnerOrderId, pgToken);
 
             response.sendRedirect(
                     FRONT_DOMAIN + "/ticketing/" + result.getAmateurShowId() + "?payment=success"
             );
+        } catch (Exception e) {
+            log.error("Payment approval redirect failed for partnerOrderId={}", partnerOrderId, e);
+            response.sendRedirect(FRONT_DOMAIN + "?payment=fail");
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java` around
lines 36 - 47, The approve method lacks error handling: wrap the call to
kakaoPayBusinessService.completePayment(partnerOrderId, pgToken) inside a
try-catch in the approve(...) controller, and on success keep the existing
response.sendRedirect(FRONT_DOMAIN + "/ticketing/" + result.getAmateurShowId() +
"?payment=success"); but on any exception catch it and redirect the user to the
same ticketing page (or a safe fallback) with an error query like
"?payment=fail" (or "?payment=error") to mirror cancel/fail behavior; ensure you
catch Exception, log the error, and still call response.sendRedirect with the
error indicator so users never see a raw 500.
🧹 Nitpick comments (1)
src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java (1)

190-214: Consider using findWithTicketAndShowById to avoid lazy-loading overhead.

Line 194 uses findById, but line 197 immediately navigates tempTicket.getAmateurRound().getAmateurShow().getId() — a chain of lazy associations. Since the repository already has findWithTicketAndShowById with an @EntityGraph that eagerly fetches amateurRound and amateurTicket.amateurShow, switching to it would eliminate the extra queries (especially relevant since the scheduler calls this method repeatedly).

♻️ Proposed refactor
-        TempTicket tempTicket = tempTicketRepository.findById(ticketId)
-                                                          .orElseThrow(() -> new GeneralException(ErrorStatus.TEMP_TICKET_NOT_FOUND));
+        TempTicket tempTicket = tempTicketRepository.findWithTicketAndShowById(ticketId)
+                                                          .orElseThrow(() -> new GeneralException(ErrorStatus.TEMP_TICKET_NOT_FOUND));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java`
around lines 190 - 214, The stopPayment method currently uses
tempTicketRepository.findById which triggers lazy-loading when accessing
tempTicket.getAmateurRound().getAmateurShow().getId(); replace the findById call
with tempTicketRepository.findWithTicketAndShowById(partnerOrderIdLong) (or the
equivalent signature) so the amateurRound and amateurShow are eagerly fetched
via the repository's `@EntityGraph`, preserving the existing orElseThrow(()-> new
GeneralException(ErrorStatus.TEMP_TICKET_NOT_FOUND)) behavior; keep the rest of
the logic (checking ReservationStatus,
amateurRoundsRepository.increaseStock(...), and
tempTicket.updateReservationStatus(...)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java`:
- Around line 50-62: The cancel and fail GET handlers (methods cancel and fail)
call kakaoPayBusinessService.stopPayment using an unauthenticated
partner_order_id, allowing attackers to expire tickets by guessing IDs; modify
preparePayment to emit a short-lived signed token/HMAC tied to partner_order_id
(and optionally timestamp/nonce) and include it in the redirect URL, then
validate that token in the cancel and fail endpoints before calling stopPayment
(reject or ignore requests with missing/invalid/expired tokens); ensure
validation uses a server-side secret and check signature + expiry to prevent
replay.
- Around line 52-62: Add structured logging to KakaoPayController by annotating
the class with a logging annotation (e.g., `@Slf4j`) and replace any
e.printStackTrace() calls in the cancel method (and the similar block at lines
67-77) with log.error statements that include a clear message and the exception
(e.g., log.error("Failed to cancel payment for partnerOrderId={}, redirecting to
front", partnerOrderId, e)); ensure you import/use the same logger across the
class so exceptions are captured by the app's logging framework.
- Around line 23-25: Replace the hardcoded FRONT_DOMAIN constant with an
externalized property: remove the private static final String FRONT_DOMAIN =
"https://seeatheater.site" and restore an `@Value-injected` field in
KakaoPayController (e.g., add `@Value`("${front.domain}") private String
FRONT_DOMAIN) so the controller reads the URL from configuration/environment;
add the property mapping in application.yml (front.domain: ${FRONT_DOMAIN}) or
ensure FRONT_DOMAIN is provided as an env var, and update any references to use
the injected FRONT_DOMAIN field.

In `@src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java`:
- Around line 190-192: The code currently calls Long.valueOf(partnerOrderId) in
stopPayment which will throw NumberFormatException for non-numeric input; wrap
the parse in a try-catch, catch NumberFormatException, log a clear message
including the offending partnerOrderId and the exception, and then return a safe
failure (e.g., throw an IllegalArgumentException or a domain-specific exception)
instead of allowing the unchecked exception to propagate silently; update
callers if needed so the scheduler and controller handle the returned/raised
error consistently.

In `@src/main/java/cc/backend/scheduler/TempTicketExpireScheduler.java`:
- Around line 27-28: The scheduler in TempTicketExpireScheduler uses
LocalDateTime.now(ZoneId.of("Asia/Seoul")) to compute expireTime while persisted
createdAt in BaseEntity is timezone-unaware (JVM default/UTC), causing a 9-hour
mismatch; change the scheduler to use the same timezone as persistence (replace
LocalDateTime.now(ZoneId.of("Asia/Seoul")) and expireTime calculation with
LocalDateTime.now() if your JVM/database uses UTC) or alternatively configure
hibernate.jdbc.time_zone to a fixed zone and make both BaseEntity persistence
and TempTicketExpireScheduler use that same zone consistently; update any
related tests/comments to reflect the chosen approach.

In `@src/main/java/cc/backend/ticket/repository/TempTicketRepository.java`:
- Around line 39-45: The JPQL in TempTicketRepository.findExpiredPendingTickets
uses the string literal 'PENDING' which is brittle; update the query to
reference the enum constant directly (e.g., use the fully qualified enum
reference like YourReservationStatusEnumType.PENDING in the WHERE clause) so the
JPQL compares against the enum constant rather than a string, or if you prefer a
reusable API, change the method signature to accept a ReservationStatus
parameter (ReservationStatus status, LocalDateTime expireTime) and use that
parameter in the WHERE clause and pass ReservationStatus.PENDING from the
scheduler; adjust imports and tests accordingly.

---

Outside diff comments:
In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java`:
- Around line 36-47: The approve method lacks error handling: wrap the call to
kakaoPayBusinessService.completePayment(partnerOrderId, pgToken) inside a
try-catch in the approve(...) controller, and on success keep the existing
response.sendRedirect(FRONT_DOMAIN + "/ticketing/" + result.getAmateurShowId() +
"?payment=success"); but on any exception catch it and redirect the user to the
same ticketing page (or a safe fallback) with an error query like
"?payment=fail" (or "?payment=error") to mirror cancel/fail behavior; ensure you
catch Exception, log the error, and still call response.sendRedirect with the
error indicator so users never see a raw 500.

---

Nitpick comments:
In `@src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java`:
- Around line 190-214: The stopPayment method currently uses
tempTicketRepository.findById which triggers lazy-loading when accessing
tempTicket.getAmateurRound().getAmateurShow().getId(); replace the findById call
with tempTicketRepository.findWithTicketAndShowById(partnerOrderIdLong) (or the
equivalent signature) so the amateurRound and amateurShow are eagerly fetched
via the repository's `@EntityGraph`, preserving the existing orElseThrow(()-> new
GeneralException(ErrorStatus.TEMP_TICKET_NOT_FOUND)) behavior; keep the rest of
the logic (checking ReservationStatus,
amateurRoundsRepository.increaseStock(...), and
tempTicket.updateReservationStatus(...)) unchanged.

Comment on lines +23 to +25
// private static final String FRONT_DOMAIN = "http://localhost:5173";
private static final String FRONT_DOMAIN = "https://seeatheater.site";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded production URL replaces previously externalized configuration — this breaks environment portability.

Replacing the @Value-injected URL with a hardcoded "https://seeatheater.site" means local development, staging, and other environments all redirect to production. The commented-out localhost line on line 24 confirms this is being toggled manually, which is error-prone and could easily leak into production with the wrong value.

Re-externalize this to application.yml (or environment variable), as it was before:

🐛 Proposed fix

In application.yml, add:

front:
  domain: ${FRONT_DOMAIN}

In the controller:

-//    private static final String FRONT_DOMAIN = "http://localhost:5173";
-    private static final String FRONT_DOMAIN = "https://seeatheater.site";
+    `@Value`("${front.domain}")
+    private String frontDomain;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java` around
lines 23 - 25, Replace the hardcoded FRONT_DOMAIN constant with an externalized
property: remove the private static final String FRONT_DOMAIN =
"https://seeatheater.site" and restore an `@Value-injected` field in
KakaoPayController (e.g., add `@Value`("${front.domain}") private String
FRONT_DOMAIN) so the controller reads the URL from configuration/environment;
add the property mapping in application.yml (front.domain: ${FRONT_DOMAIN}) or
ensure FRONT_DOMAIN is provided as an env var, and update any references to use
the injected FRONT_DOMAIN field.

Comment on lines 50 to 62
@GetMapping("/cancel")
@Operation(summary = "카카오페이 결제 중단 취소 (자동 호출)", description = "결제 중단 시 카카오 서버에서 cancel_url로 자동 호출되는 API입니다. 직접 호출하지 마세요.")
public ApiResponse<String> cancel(@RequestParam("partner_order_id") String partnerOrderId) {
public void cancel(@RequestParam("partner_order_id") String partnerOrderId, HttpServletResponse response) throws IOException{
try {
kakaoPayBusinessService.stopPayment(partnerOrderId);
return ApiResponse.onSuccess("결제가 취소되었습니다.");
} catch (NumberFormatException e) {
return ApiResponse.onFailure("INVALID_ORDER_ID", "유효하지 않은 주문번호입니다.", null);
Long playId = kakaoPayBusinessService.stopPayment(partnerOrderId);
response.sendRedirect(
FRONT_DOMAIN + "/ticketing/" + playId + "?payment=cancel"
);
} catch (Exception e) {
e.printStackTrace();
response.sendRedirect(FRONT_DOMAIN);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unauthenticated cancel and fail endpoints allow any caller to expire arbitrary pending tickets.

These GET endpoints accept a partner_order_id without authentication and call stopPayment, which restores inventory and marks the ticket as EXPIRED. If an attacker guesses or enumerates ticket IDs, they can force-expire other users' in-flight payments.

While the retrieved learning confirms these are browser redirect URLs (not server-to-server), consider adding a lightweight verification — e.g., a signed token or HMAC in the redirect URL generated during preparePayment, validated here before calling stopPayment.

Also applies to: 65-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java` around
lines 50 - 62, The cancel and fail GET handlers (methods cancel and fail) call
kakaoPayBusinessService.stopPayment using an unauthenticated partner_order_id,
allowing attackers to expire tickets by guessing IDs; modify preparePayment to
emit a short-lived signed token/HMAC tied to partner_order_id (and optionally
timestamp/nonce) and include it in the redirect URL, then validate that token in
the cancel and fail endpoints before calling stopPayment (reject or ignore
requests with missing/invalid/expired tokens); ensure validation uses a
server-side secret and check signature + expiry to prevent replay.

Comment on lines +52 to 62
public void cancel(@RequestParam("partner_order_id") String partnerOrderId, HttpServletResponse response) throws IOException{
try {
kakaoPayBusinessService.stopPayment(partnerOrderId);
return ApiResponse.onSuccess("결제가 취소되었습니다.");
} catch (NumberFormatException e) {
return ApiResponse.onFailure("INVALID_ORDER_ID", "유효하지 않은 주문번호입니다.", null);
Long playId = kakaoPayBusinessService.stopPayment(partnerOrderId);
response.sendRedirect(
FRONT_DOMAIN + "/ticketing/" + playId + "?payment=cancel"
);
} catch (Exception e) {
e.printStackTrace();
response.sendRedirect(FRONT_DOMAIN);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace e.printStackTrace() with proper logging.

e.printStackTrace() writes to stderr and bypasses the application's logging framework (log level, format, aggregation). The class doesn't even have @Slf4j. Use a logger instead so these errors are captured in your structured logs.

♻️ Proposed fix

Add @Slf4j to the class, then:

         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Payment cancel failed for partnerOrderId={}", partnerOrderId, e);
             response.sendRedirect(FRONT_DOMAIN);
         }
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Payment fail handling failed for partnerOrderId={}", partnerOrderId, e);
             response.sendRedirect(FRONT_DOMAIN);
         }

Also applies to: 67-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/controller/KakaoPayController.java` around
lines 52 - 62, Add structured logging to KakaoPayController by annotating the
class with a logging annotation (e.g., `@Slf4j`) and replace any
e.printStackTrace() calls in the cancel method (and the similar block at lines
67-77) with log.error statements that include a clear message and the exception
(e.g., log.error("Failed to cancel payment for partnerOrderId={}, redirecting to
front", partnerOrderId, e)); ensure you import/use the same logger across the
class so exceptions are captured by the app's logging framework.

Comment on lines +190 to 192
public Long stopPayment(String partnerOrderId) {

Long ticketId = Long.valueOf(partnerOrderId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Long.valueOf on an invalid partnerOrderId will throw an unchecked NumberFormatException.

If partnerOrderId is not a valid numeric string (e.g., corrupted or tampered input), Long.valueOf on line 192 throws NumberFormatException, which propagates unhandled. In the controller's cancel/fail endpoints this is caught by the generic catch (Exception e), but in the scheduler path it could log-and-continue. Still, a more explicit handling or logging of this parse step would make debugging easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/kakaoPay/service/KakaoPayBusinessService.java`
around lines 190 - 192, The code currently calls Long.valueOf(partnerOrderId) in
stopPayment which will throw NumberFormatException for non-numeric input; wrap
the parse in a try-catch, catch NumberFormatException, log a clear message
including the offending partnerOrderId and the exception, and then return a safe
failure (e.g., throw an IllegalArgumentException or a domain-specific exception)
instead of allowing the unchecked exception to propagate silently; update
callers if needed so the scheduler and controller handle the returned/raised
error consistently.

Comment on lines +27 to +28
LocalDateTime nowKst = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
LocalDateTime expireTime = nowKst.minusMinutes(15);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate TempTicket entity
echo "=== Finding TempTicket entity ==="
fd -e java -path "*/entity/*" | xargs grep -l "class TempTicket" 2>/dev/null || rg -l "class TempTicket" --type=java

echo ""
echo "=== TempTicket entity - createdAt field definition ==="
rg "class TempTicket" -A 50 --type=java | head -100

echo ""
echo "=== Check for Hibernate/JDBC timezone configuration ==="
fd -e properties -o -e yml -o -e yaml | xargs grep -i "time.*zone\|jdbc.time_zone" 2>/dev/null || echo "No explicit timezone config found"

echo ""
echo "=== Check application.yml for timezone settings ==="
fd application.yml -o -path "*application*" -name "*.yml" -o -name "*.yaml" | head -5 | xargs cat 2>/dev/null | grep -i "timezone\|time-zone\|jackson\|jpa\|hibernate" || echo "Checking application.yml..."

echo ""
echo "=== Check for `@CreatedDate`, `@CreationTimestamp`, or `@Temporal` annotations ==="
rg "@CreatedDate|@CreationTimestamp|@Temporal|@Column.*columnDefinition" --type=java -B2 -A2

echo ""
echo "=== Check `@EnableScheduling` configuration ==="
rg "@EnableScheduling" --type=java

Repository: SeeATheater/CC_Backend

Length of output: 14675


🏁 Script executed:

cat -n src/main/java/cc/backend/ticket/entity/TempTicket.java

Repository: SeeATheater/CC_Backend

Length of output: 2642


🏁 Script executed:

cat -n src/main/java/cc/backend/domain/common/BaseEntity.java

Repository: SeeATheater/CC_Backend

Length of output: 896


🏁 Script executed:

find . -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" | head -5 | xargs cat 2>/dev/null | head -100

Repository: SeeATheater/CC_Backend

Length of output: 2030


🏁 Script executed:

rg "LocalDateTime.now()" --type=java -C2 | head -30

Repository: SeeATheater/CC_Backend

Length of output: 3183


Critical timezone mismatch: scheduler uses explicit Asia/Seoul zone while database stores createdAt in JVM default timezone (typically UTC).

LocalDateTime.now(ZoneId.of("Asia/Seoul")) produces a KST-aware timestamp, but the @CreatedDate field in BaseEntity persists createdAt as timezone-unaware LocalDateTime, using the JVM's default timezone (UTC in most cloud deployments). The jackson.time-zone: Asia/Seoul setting in application.yml only affects JSON serialization, not JPA persistence.

When the scheduler compares a KST-based expireTime against UTC-stored createdAt values, timestamps appear ~9 hours older than they actually are, causing tickets to expire prematurely.

Use the same timezone as the persistence layer: either LocalDateTime.now() (matching JVM default) if the database stores in UTC, or explicitly configure hibernate.jdbc.time_zone to ensure consistency.

🐛 Proposed fix (if JVM default is UTC)
-        LocalDateTime nowKst = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
-        LocalDateTime expireTime = nowKst.minusMinutes(15);
+        LocalDateTime expireTime = LocalDateTime.now().minusMinutes(15);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LocalDateTime nowKst = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
LocalDateTime expireTime = nowKst.minusMinutes(15);
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(15);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/scheduler/TempTicketExpireScheduler.java` around
lines 27 - 28, The scheduler in TempTicketExpireScheduler uses
LocalDateTime.now(ZoneId.of("Asia/Seoul")) to compute expireTime while persisted
createdAt in BaseEntity is timezone-unaware (JVM default/UTC), causing a 9-hour
mismatch; change the scheduler to use the same timezone as persistence (replace
LocalDateTime.now(ZoneId.of("Asia/Seoul")) and expireTime calculation with
LocalDateTime.now() if your JVM/database uses UTC) or alternatively configure
hibernate.jdbc.time_zone to a fixed zone and make both BaseEntity persistence
and TempTicketExpireScheduler use that same zone consistently; update any
related tests/comments to reflect the chosen approach.

Comment on lines +39 to +45
@Query("""
SELECT t
FROM TempTicket t
WHERE t.reservationStatus = 'PENDING'
AND t.createdAt < :expireTime
""")
List<TempTicket> findExpiredPendingTickets(LocalDateTime expireTime);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find ReservationStatus enum definition
fd "ReservationStatus.java" --type f

Repository: SeeATheater/CC_Backend

Length of output: 133


🏁 Script executed:

# Find TempTicket entity definition
fd "TempTicket.java" --type f

Repository: SeeATheater/CC_Backend

Length of output: 120


🏁 Script executed:

# Search for `@Enumerated` annotations in the codebase to understand mapping patterns
rg "@Enumerated" --type java -B 2 -A 2

Repository: SeeATheater/CC_Backend

Length of output: 4719


🏁 Script executed:

# Search for any usage of findExpiredPendingTickets method
rg "findExpiredPendingTickets" --type java

Repository: SeeATheater/CC_Backend

Length of output: 350


🏁 Script executed:

# Read ReservationStatus enum definition
cat "src/main/java/cc/backend/ticket/entity/enums/ReservationStatus.java"

Repository: SeeATheater/CC_Backend

Length of output: 189


🏁 Script executed:

# Read TempTicket entity definition
cat "src/main/java/cc/backend/ticket/entity/TempTicket.java"

Repository: SeeATheater/CC_Backend

Length of output: 2075


🏁 Script executed:

# Read TempTicketExpireScheduler to see usage context
cat "src/main/java/cc/backend/scheduler/TempTicketExpireScheduler.java"

Repository: SeeATheater/CC_Backend

Length of output: 1793


Use enum reference instead of string literal for type safety.

The string literal 'PENDING' in JPQL is fragile — if the enum constant name or @Enumerated mapping strategy changes, this query breaks silently at runtime. While the current @Enumerated(EnumType.STRING) mapping makes the comparison work, it lacks compile-time type safety.

Since the method name findExpiredPendingTickets and the scheduler usage indicate that PENDING is intentionally hardcoded (not parameterizable), replace the string literal with a fully qualified enum reference:

     `@Query`("""
     SELECT t 
     FROM TempTicket t
-    WHERE t.reservationStatus = 'PENDING'
+    WHERE t.reservationStatus = cc.backend.ticket.entity.enums.ReservationStatus.PENDING
     AND t.createdAt < :expireTime
     """)
     List<TempTicket> findExpiredPendingTickets(LocalDateTime expireTime);

Alternatively, if the method should accept any status, use a parameter and update the signature:

List<TempTicket> findExpiredPendingTickets(ReservationStatus status, LocalDateTime expireTime);

Then pass ReservationStatus.PENDING from the scheduler. However, this changes the API contract—keep the current approach if hardcoding PENDING is the intended design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/cc/backend/ticket/repository/TempTicketRepository.java` around
lines 39 - 45, The JPQL in TempTicketRepository.findExpiredPendingTickets uses
the string literal 'PENDING' which is brittle; update the query to reference the
enum constant directly (e.g., use the fully qualified enum reference like
YourReservationStatusEnumType.PENDING in the WHERE clause) so the JPQL compares
against the enum constant rather than a string, or if you prefer a reusable API,
change the method signature to accept a ReservationStatus parameter
(ReservationStatus status, LocalDateTime expireTime) and use that parameter in
the WHERE clause and pass ReservationStatus.PENDING from the scheduler; adjust
imports and tests accordingly.

@ddhi7 ddhi7 merged commit 2f67096 into develop Feb 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant